package org.mockfu;

import static org.objectweb.asm.Opcodes.*;

import java.util.LinkedList;
import java.util.List;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

/**
 * A <tt>MethodVisitor</tt> which facilitates replacing constructor calls with other bytecodes. A constructor
 * invocation starts with an object instantiation, followed by the creation of copies of the object, set up of
 * constructor arguments, and invocation of the constructor. To replace these bytecodes with instantiation through
 * reflection you would need to set up constructor arguments first, invoke the constructor, then make copies of the
 * object. A <tt>ConstructorReplacement</tt> tracks and delays the process of constructor invocation to enable this
 * kind of reordering.
 */
class ConstructorReplacementVisitor implements MethodVisitor
{

    private static interface Instruction
    {
        public void visit();
    }

    private final MethodVisitor mv;
    private final List<Instruction> instructions = new LinkedList<Instruction>();

    private String typeDesc;
    private int stack = 0;
    private int dupCount = 0;

    public ConstructorReplacementVisitor(MethodVisitor delegate)
    {
        this.mv = delegate;
    }

    /**
     * Plays back the set up of the constructor arguments.
     *
     * @return the original MethodVisitor that this decorates
     */
    public MethodVisitor playbackArguments()
    {
        /* skip first instruction (new) and any dups */
        int i = 0;
        for (Instruction instruction : instructions)
        {
            if (i++ >= dupCount+1)
                instruction.visit();
        }

        return mv;
    }

    /**
     * Plays back the object instantiation and duplication.
     *
     * @return the original MethodVisitor that this decorates
     */
    public MethodVisitor playbackInstantiation()
    {
        mv.visitTypeInsn(NEW, typeDesc);
        for (int i = 0; i < dupCount; i++)
            mv.visitInsn(DUP);

        return mv;
    }

    /**
     * Given a single object on the stack, adjusts the number of instances to that of the constructed object whose
     * constructor is being replaced.
     */
    public void adjustCopies()
    {
        int dupCount = this.dupCount;
        if (dupCount == 0)
            mv.visitInsn(POP);
        else
            while (dupCount-- > 1)
                mv.visitInsn(DUP);
    }

    /**
     * Plays back all recorded instructions. Effectively cancels the use of this visitor.
     *
     * @return the original MethodVisitor that this decorates
     */
    public MethodVisitor playback()
    {
        for (Instruction instruction : instructions)
        {
            instruction.visit();
        }
        return mv;
    }


    public AnnotationVisitor visitAnnotation(String desc, boolean visible)
    {
        return mv.visitAnnotation(desc, visible);
    }

    public AnnotationVisitor visitAnnotationDefault()
    {
        return mv.visitAnnotationDefault();
    }

    public void visitAttribute(Attribute attr)
    {
        mv.visitAttribute(attr);
    }

    public void visitCode()
    {
        visitCode();
    }

    public void visitEnd()
    {
        visitEnd();
    }

    public void visitFieldInsn(final int opcode, final String owner, final String name, final String desc)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitFieldInsn(opcode, owner, name, desc);
            }
        });

        switch (opcode)
        {
        case GETSTATIC:
            stack++;
            break;
        case PUTSTATIC:
            stack--;
            break;
        case PUTFIELD:
            stack -= 2;
            break;
        }
    }

    public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2)
    {
        visitFrame(type, local, local2, stack, stack2);
    }

    public void visitIincInsn(final int var, final int increment)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitIincInsn(var, increment);
            }
        });

        /* no stack change */
    }

    public void visitInsn(final int opcode)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitInsn(opcode);
            }
        });

        switch(opcode)
        {
        case ACONST_NULL:
        case ICONST_M1:
        case ICONST_0:
        case ICONST_1:
        case ICONST_2:
        case ICONST_3:
        case ICONST_4:
        case ICONST_5:
        case LCONST_0:
        case LCONST_1:
        case FCONST_0:
        case FCONST_1:
        case FCONST_2:
        case DCONST_0:
        case DCONST_1:
            stack++;
            break;
        case IALOAD:
        case LALOAD:
        case FALOAD:
        case DALOAD:
        case AALOAD:
        case BALOAD:
        case CALOAD:
        case SALOAD:
            stack--;
            break;
        case IASTORE:
        case LASTORE:
        case FASTORE:
        case DASTORE:
        case AASTORE:
        case BASTORE:
        case CASTORE:
        case SASTORE:
            stack -= 3;
            break;
        case POP:
            stack--;
            break;
        case DUP:
            if (stack-1 == dupCount)
            {
                stack++;
                dupCount++;
            }
            break;
        case POP2:
        case DUP_X1:
        case DUP_X2:
        case DUP2:
        case DUP2_X1:
        case DUP2_X2:
            throw new RuntimeException("wtf");
        case IADD:
        case LADD:
        case FADD:
        case DADD:
        case ISUB:
        case LSUB:
        case FSUB:
        case DSUB:
        case IMUL:
        case LMUL:
        case FMUL:
        case DMUL:
        case IDIV:
        case LDIV:
        case FDIV:
        case DDIV:
        case IREM:
        case LREM:
        case FREM:
        case DREM:
        case ISHL:
        case LSHL:
        case ISHR:
        case LSHR:
        case IUSHR:
        case LUSHR:
        case IAND:
        case LAND:
        case IOR:
        case LOR:
        case IXOR:
        case LXOR:
        case LCMP:
        case FCMPL:
        case FCMPG:
        case DCMPL:
        case DCMPG:
            stack--;
            break;
        case IRETURN:
        case LRETURN:
        case FRETURN:
        case DRETURN:
        case ARETURN:
        case RETURN:
            throw new IllegalStateException("return before constructor <init>");
        case MONITORENTER:
        case MONITOREXIT:
            stack--;
            break;
        }
    }

    public void visitIntInsn(final int opcode, final int operand)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitIntInsn(opcode, operand);
            }
        });

        if (opcode == BIPUSH || opcode == SIPUSH)
            stack++;
    }

    public void visitJumpInsn(final int opcode, final Label label)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitJumpInsn(opcode, label);
            }
        });

        /* yeah, i don't know ... */
    }

    public void visitLabel(final Label label)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitLabel(label);
            }
        });
    }

    public void visitLdcInsn(final Object cst)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitLdcInsn(cst);
            }
        });

        stack++;
    }

    public void visitLineNumber(final int line, final Label start)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitLineNumber(line, start);
            }
        });
    }

    public void visitLocalVariable(final String name, final String desc, final String signature, final Label start,
                                   final Label end, final int index)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitLocalVariable(name, desc, signature, start, end, index);
            }
        });
    }

    public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitLookupSwitchInsn(dflt, keys, labels);
            }
        });
    }

    public void visitMaxs(int maxStack, int maxLocals)
    {
        mv.visitMaxs(maxStack, maxLocals);
    }

    public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitMethodInsn(opcode, owner, name, desc);
            }
        });

        stack -= Type.getArgumentTypes(desc).length;
        if (Type.getReturnType(desc) != Type.VOID_TYPE)
            stack++;
        if (opcode != INVOKESTATIC)
            stack--;
    }

    public void visitMultiANewArrayInsn(final String desc, final int dims)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitMultiANewArrayInsn(desc, dims);
            }
        });


    }

    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible)
    {
        return mv.visitParameterAnnotation(parameter, desc, visible);
    }

    public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label[] labels)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitTableSwitchInsn(min, max, dflt, labels);
            }
        });
    }

    public void visitTryCatchBlock(Label start, Label end, Label handler, String type)
    {
        mv.visitTryCatchBlock(start, end, handler, type);
    }

    public void visitTypeInsn(final int opcode, final String desc)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitTypeInsn(opcode, desc);
            }
        });

        if (opcode == NEW)
        {
            if (typeDesc == null)
                typeDesc = desc;
            stack++;
        }
    }

    public void visitVarInsn(final int opcode, final int var)
    {
        instructions.add(new Instruction() {
            public void visit()
            {
                mv.visitVarInsn(opcode, var);
            }
        });

        switch (opcode)
        {
        case ILOAD:
        case LLOAD:
        case FLOAD:
        case DLOAD:
        case ALOAD:
            stack++;
            break;
        case ISTORE:
        case LSTORE:
        case FSTORE:
        case DSTORE:
        case ASTORE:
            stack--;
            break;
        }
    }

}
